package fregat.connector.worker;

import fregat.base.database.DatabaseConnector;
import fregat.connector.ConnectorService;

import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Types;

/**
 * Worker that implements the send logic.
 *
 * @author Alexander Mann <br><b><i>ECO_L O G</i> &nbsp Logistiksysteme GmbH</b>
 * @version 1.0
 */
public class SendWorker extends AlertWorker {
  public static final String SEND = "send"; // alert message for direct sending

  private long timeout; // time to wait for quit before resending
  private int maxRetries; // number of send retries
  private boolean verified; // true, when data is verified by communication partner
  private boolean pending; // true, when telegram is pending

  /**
   * Creates a new send worker, which has the following logic.
   * <li>waits for database alert to process and send data
   * (once done after startup without alert)</li>
   * <li>retrieves the IDs of data that should be processed by executing
   * the configured retrieve select</li>
   * <li>processes each ID by calling the configured process procedure</li>
   * <li>retrieves the IDs of data that should be sent by executing
   * the configured data procedure</li>
   * <li>retrieves data to send for each ID by executing the configured
   * get select</li>
   * <li>if configured waits for quit telegram from communication partner and
   * verifies quit by executing the configured quit procedure</li>
   * <li>resends same data if timeout expires</li>
   * <li>resends other data if quit procedure returns other ID than sent data</li>
   * <li>reprocesses and resends if quit procedure returns 0</li>
   * <br>If no retrieve select is configured the data select is used for retrieving
   * data which is sent without processing.
   *
   * @param name Configuration name of worker
   */
  public SendWorker(String name) {
    super(name);
    String configPath = "worker.config." + getName();

    // configure retries -->
    maxRetries = configuration.getConfigurationInteger(configPath + ".retries");
    if (maxRetries < 0) {
      maxRetries = 5;
    }
    // <--

    // configure timeout -->
    timeout = configuration.getConfigurationLong(configPath + ".timeout");
    if (timeout > 0) {
      timeout *= 1000;
    } else {
      timeout = 5000;
    }
    // <--
  }

  /**
   * Stops waiting for quit and database alert and disconnects from database.
   */
  public synchronized void shutdown() {
    super.shutdown();
    notify();
  }

  public synchronized void connected(boolean b) {
    super.connected(b);
    if (!b) {
      notify();
    }
  }

  /**
   * This method is called when data should be processed and sent.
   *
   * @param message Message of alert
   */
  protected boolean alertReceived(String message) {
    // send all existing data
    boolean processing = transferData();

    if (connected && retrieveConfigured) {
      // process and send data
      processing = super.alertReceived(message);
    } else {
      // inform channel that data was completely processed
      ConnectorService.dataComplete(channelName);
    }

    return processing;
  }

  /**
   * Sends the specified data to the communication partner and waits for quit
   * (if configured). Resends after timeout expired.
   *
   * @param data Data to send
   * @return True, if success
   */
  protected synchronized boolean transferData(String data) {
    verified = false;
    int retries = 1;

    while (!verified && !shutdown && connected) {
      long currentID = dataID;
      ConnectorService.debug(getName() + ", " +
        ConnectorService.resource.getString("SendWorker_transferData_1") +
        " #" + dataID + "(" + retries + ")");
      pending = true;

      boolean sendOk = send(channelName, data);
      
      if (sendOk) {
        retries++;
      }

      // wait for quit
      if (quitSQL != null && !verified && !shutdown) {
        try {
          wait(timeout);
        } catch (InterruptedException ex) {
          ex.printStackTrace();
        }

      } else {
        // no quit procedure configured --> do not wait for quit
        verified = sendOk;
      }


      if (!verified && dataID == currentID && retries > maxRetries) {
        long result = 0;
        try {
          // maximum number of retries reached
          ConnectorService.debug(getName() + ", " +
            ConnectorService.resource.getString("SendWorker_transferData_2"));
          result = processQuit(-1, "");

        } catch (SQLException ex) {
          ConnectorService.error("SendWorker.transferData", ex);
          databaseConnector.rollback();
        }

        if (result >= 0) {
          dataID = result;
          if (dataID == currentID) {
            verified = true;
            ConnectorService.debug(getName() + ", " +
              ConnectorService.resource.getString("SendWorker_transferData_3") +
              " #" + dataID);
          }
        } else {
          retries = 1;
          ConnectorService.debug(getName() + ", " +
            ConnectorService.resource.getString("SendWorker_transferData_4") +
            " #" + dataID);
        }
      }

      if (verified && dataID > 0 && dataID != currentID) {
        // send modified data with different ID
        ConnectorService.note(getName() + ", currentID=" + currentID + ",dataID=" + dataID);
        try {
          data = getData(dataID);
          currentID = dataID;
          retries = 1;
          verified = false;
        } catch (SQLException ex) {
          ConnectorService.error("SendWorker.transferData", ex);
        }
      }

    }

    if (dataID == 0) {
      // retry with new processing
      verified = false;
    }


    return verified;
  }

  /**
   * Transfers received data to the database by executing the configured quit
   * function.
   *
   * @param channelName  Configuration name of channel where data was received
   * @param receivedData Data that was received
   */
  protected synchronized boolean dataReceived(String channelName, String receivedData) {
    boolean receiveOk = true;

    if (!shutdown && quitSQL != null && pending) {
      // process received quit in database -->
      receiveOk = super.dataReceived(channelName, receivedData);
           
      try {

        long result = processQuit(0, receivedData);

        if (result >= 0) {
          pending = false;

          String debug = ConnectorService.resource.getString("SendWorker_dataReceived_1") +
            " #" + dataID;

          if (result == dataID) {
            debug = ConnectorService.resource.getString("SendWorker_dataReceived_2") +
              " " + debug;
          } else {
            debug = ConnectorService.resource.getString("SendWorker_dataReceived_3") +
              " " + debug + ", " +
              ConnectorService.resource.getString("SendWorker_dataReceived_4")
              .replaceFirst("%1", Long.toString(result));
          }
          ConnectorService.debug(getName() + ", " + debug);


          dataID = result;
          verified = true;

          receiveOk = true;
        }
        notify();

      } catch (SQLException ex) {
        databaseConnector.rollback();
        ConnectorService.error(getName() + ", SendWorker.dataReceived", ex);
        receiveOk = false;
      }
      // <--
    }

    if (shutdown) {
      notify();
    }
    return receiveOk;
  }

  /**
   * Calls the quit function to handle a received quit or to inform database that
   * sending a telegram has reached the retry limit.
   * <li>
   * Return value matches the ID of the sent data if quit is positive.
   * </li>
   * <li>
   * Return value is greater than 0 but does not match the ID of the sent data
   * if quit is negative and modified data with ID of the return value has to be
   * resent immediately.
   * </li>
   * <li>
   * Return value is 0, when quit was negative but data has not to be resent.
   * </li>
   * <li>
   * Return value is negative, if received telegram is no quit (Data is resent
   * after reaching timeout) or retries are reset to 0.
   * </li>
   *
   * @param level        Level of quit (0 to handle quit,-1 if retry limit reached)
   * @param receivedData Data that was received (empty when level = -1)
   * @return ID of sent data (positive),0 (negative),-1 (no quit),other ID than sent
   * @throws SQLException
   */
  private long processQuit(int level, String receivedData) throws SQLException {
    CallableStatement statement = databaseConnector.getStatement(quitKey);

    if (statement == null) {
      statement = databaseConnector.prepareStatement(quitKey, quitSQL);
      statement.registerOutParameter(1, Types.INTEGER);
      statement.registerOutParameter(4, Types.VARCHAR);
    }

    statement.setLong(2, dataID);
    statement.setInt(3, level);
    statement.setString(4, receivedData);

    executeStatement(DatabaseConnector.
      logStatement(quitKey, new String[]{new Long(dataID).toString(),
                                         Integer.toString(level), receivedData}),
      quitKey);

    databaseConnector.commit();

    return statement.getLong(1);
  }
}